__author__ = 'Brandon Corfman'
import random, textwrap
import term
from player import Player
from dwarves import Saboteur, GoldDigger
from humanagent import HumanAgent
from computeragent import ComputerAgent
from path import Path
from actions import BrokenTool, RockFall, RepairOne, RepairAny, Peek
from board import Board

class Game(object):
    def __init__(self):
        """
        A software implementation of the card game Saboteur by Fredric Moyersoen.
        A human can choose to play against another human or against the (eventual)
        computer AI.

        Written by Brandon Corfman, July 12, 2011.
        """
        self.dwarf_types = {3: [Saboteur, GoldDigger, GoldDigger, GoldDigger],
                            4: [Saboteur, GoldDigger, GoldDigger, GoldDigger, GoldDigger],
                            5: [Saboteur, Saboteur, GoldDigger, GoldDigger, GoldDigger, GoldDigger],
                            6: [Saboteur, Saboteur, GoldDigger, GoldDigger, GoldDigger,
                                GoldDigger, GoldDigger],
                            7: [Saboteur, Saboteur, Saboteur, GoldDigger, GoldDigger,
                                GoldDigger, GoldDigger, GoldDigger],
                            8: [Saboteur, Saboteur, Saboteur, GoldDigger, GoldDigger,
                                GoldDigger, GoldDigger, GoldDigger, GoldDigger],
                            9: [Saboteur, Saboteur, Saboteur, GoldDigger, GoldDigger,
                                GoldDigger, GoldDigger, GoldDigger, GoldDigger, GoldDigger],
                            10: [Saboteur, Saboteur, Saboteur, Saboteur, GoldDigger, GoldDigger,
                                 GoldDigger, GoldDigger, GoldDigger, GoldDigger, GoldDigger]}
        self.paths = [Path('NSE', ''), Path('NS', ''), Path('N', 'S'),
                      Path('E','W'), Path('EW', ''), Path('NEW', ''),
                      Path('NW', ''), Path('NSEW', ''), Path('NSEW', 'NSEW'), Path('NSEW', 'NSEW'),
                      Path('NSEW', 'NSEW'), Path('NSEW', 'NSEW'),
                      Path('NSEW', 'NSEW'), Path('SEW', 'SEW'), Path('SEW', 'SEW'),
                      Path('SEW', 'SEW'), Path('SEW', 'SEW'), Path('SEW', 'SEW'),
                      Path('NE', ''), Path('NSW', 'NSW'), Path('NSW', 'NSW'), Path('NSW', 'NSW'),
                      Path('NSW', 'NSW'), Path('NSW', 'NSW'), Path('NS', 'NS'), Path('NS', 'NS'),
                      Path('NS', 'NS'), Path('NS', 'NS'), Path('SW', 'SW'), Path('SW', 'SW'),
                      Path('SW', 'SW'), Path('SW', 'SW'),
                      Path('SW', 'SW'), Path('NW', 'NW'),
                      Path('NW', 'NW'), Path('NW', 'NW'), Path('NW', 'NW'), Path('EW', 'EW'),
                      Path('EW', 'EW'), Path('EW', 'EW')]
        self.actions = [RockFall(), RepairOne('axe'), BrokenTool('lamp'), Peek(),
                        RepairOne('cart'), BrokenTool('axe'), Peek(), RepairOne('cart'),
                        Peek(), BrokenTool('cart'), Peek(), BrokenTool('cart'), Peek(),
                        BrokenTool('cart'), RepairAny('lamp', 'cart'), RepairAny('lamp', 'cart'),
                        RepairAny('axe', 'cart'), BrokenTool('lamp'), Peek(), RepairOne('lamp'),
                        BrokenTool('axe'), RockFall(), RepairOne('lamp'), BrokenTool('axe'),
                        RockFall(), RepairOne('axe'), BrokenTool('lamp')]
        self.num_dealt = {3:6, 4:6, 5:6, 6:5, 7:5, 8:4, 9:4, 10:4}
        self.num_players = 0
        self.reset_draw_pile()
        self.reset_nuggets()
        self.board = Board()
        self.players = []
        self.agent_types = []

    def reset_nuggets(self):
        self.nuggets = [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,3,3,3,3]
        random.shuffle(self.nuggets)

    def reset_draw_pile(self):
        self.draw_pile = self.actions + self.paths
        random.shuffle(self.draw_pile)

    def print_instructions(self):
        """
        Prints the game instructions for the user.
        """
        print ("Welcome to the game of Saboteur!")
        print ("--------------------------------")
        ins = "   In Saboteur, the players take on the part of dwarves. Either they are gold-diggers, "
        ins += "working their tunnels ever deeper into the mountain in search of treasures, or they are "
        ins += "saboteurs trying to put obstacles into the diggers' path. The members of each group "
        ins += "should support each other, once they figure out who is on their side. If the gold-diggers "
        ins += "manage to create a path to the treasure, they are duly rewarded with nuggets of gold, "
        ins += "while the saboteurs have to leave empty-handed. However, should the gold-diggers fail, "
        ins += "the saboteurs reap the reward. After three rounds, the players with the most nuggets "
        ins += "wins."
        print(textwrap.fill(ins))
        print()

    def create_hand(self, num):
        """
        Extracts a hand for a player from the draw pile.
        num: Number of cards to pull off the top of the draw pile to create the hand.
        """
        hand = self.draw_pile[0:num]
        self.draw_pile = self.draw_pile[num:]
        return hand

    def add_player(self, agent_type, dwarf_type, name):
        """
        Adds a new player to the players list, assigning them a initial set
        of cards from the draw pile and assigning them a agent type (i.e., HumanPlayer
        or ComputerPlayer).
        agent_type: HumanPlayer or ComputerPlayer class.
        name: Player's first name for a HumanPlayer or just a generic name for a ComputerPlayer.
        dwarf_types: list of GoldDigger or Saboteur classes. This list contains how many of each
        type should be allowed for a particular round based on the number of players in the game.
        """
        num_deal = self.num_dealt[self.num_players]
        agent = agent_type(self.create_hand(num_deal), self.num_players)
        self.agent_types.append(agent_type)
        dwarf = dwarf_type()
        player = Player(name, agent, dwarf, self)
        agent.set_parent(player)
        self.players.append(player)

    def calc_player_scores(self):
        player = None
        start = None
        if self.board.discovered_goal:
            for start, player in enumerate(self.players):
                if player == self.board.discovered_goal:
                    if isinstance(player.dwarf, Saboteur):
                        return
                    else:
                        break
            else:
                raise (ValueError, "Start index cannot be None.")

        if player and isinstance(player.dwarf, GoldDigger):
            # Divide gold among gold-diggers.
            total_players = len(self.players)
            idx = start
            while idx < start + total_players:
                if isinstance(self.players[idx % total_players].dwarf, GoldDigger):
                    nuggets = self.nuggets.pop()
                    self.players[idx % total_players].nuggets += nuggets
                idx += 1
        else:
            # Divide gold among saboteurs
            winners = [x for x in self.players if isinstance(x.dwarf, Saboteur)]
            total = len(winners)
            nuggets = 0
            if total == 1:
                nuggets = 4
            elif total == 2 or total == 3:
                nuggets = 3
            elif total == 4:
                nuggets = 2
            for winner in winners:
                winner.nuggets += nuggets

    def print_player_scores(self, rnd):
        print("\nScores after round {0}".format(rnd))
        print("--------------------")
        name_len = 0
        for p in self.players:
            name_len = max(name_len, len(p.name + " "))
        type_len = 0
        for p in self.players:
            type_len = max(type_len, len(" (") + len(p.dwarf.__class__.__name__) + len(")"))
        name_format = '{0}'.format(name_len)
        type_format = '{0}'.format(type_len)
        format_str = '{0:' + name_format + '}{1:' + type_format + '}: {2}'
        for p in self.players:
            name_str = '{0}'.format(p.name)
            type_str = '({0})'.format(p.dwarf.__class__.__name__)
            print(format_str.format(name_str, type_str, p.nuggets))
        print()

    def setup_notifications(self):
        for player in self.players:
            for other in self.players:
                if player != other:
                    player.subscribe(other)

    def get_player_setup(self):
        """
        Asks the user for information to set up the game, i.e., number of players,
        agent type (human or computer), player name (in the case of a human player;
        automatically picks a name for a computer AI).
        """
        while not self.num_players:
            try:
                num_players = int(input('How many players (3-10)? '))
                if num_players < 3 or num_players > 10:
                    raise ValueError
                self.num_players = num_players
            except ValueError:
                pass
        index = 0
        dwarf_list = self.dwarf_types[self.num_players]
        random.shuffle(dwarf_list)
        while index < self.num_players:
            agent_str = input('Player %d type: (H)uman or (C)omputer? ' % (index + 1))
            try:
                if agent_str[0].lower() == 'h':
                    name = input("Player %d's name? " % (index + 1))
                    self.add_player(HumanAgent, dwarf_list[index], name)
                    index += 1
                elif agent_str[0].lower() == 'c':
                    name = 'Computer%d' % (index+1)
                    self.add_player(ComputerAgent, dwarf_list[index], name)
                    index += 1
            except IndexError:
                pass
        self.setup_notifications()

    def start_new_round(self):
        self.reset_draw_pile()
        dwarf_list = self.dwarf_types[self.num_players]
        random.shuffle(dwarf_list)
        random.shuffle(self.agent_types)
        num_deal = self.num_dealt[self.num_players]
        for i, p in enumerate(self.players):
            p.dwarf = dwarf_list[i]()
            p.agent = self.agent_types[i](self.create_hand(num_deal), self.num_players)
            p.agent.set_parent(p)
        self.board = Board()
        self.setup_notifications()

    def start(self):
        """
        Plays 3 rounds of the game.
        Loops over all players and calls their move method.
        Breaks out of the loop if round_ended is True.
        """
        rnd = 1
        while rnd <= 3:
            round_ended = False
            curr_idx = -1
            while not round_ended:
                curr_idx = (curr_idx + 1) % self.num_players
                curr_player = self.players[curr_idx]
                others = self.players[:]
                others.remove(curr_player)
                curr_player.move(self.board, others)
                round_ended = self.board.gold_found() or (not self.draw_pile and
                                                          all((i.passed for i in self.players)))
                if self.draw_pile:
                    curr_player.add_card_to_hand(self.draw_pile.pop())
            self.calc_player_scores()
            self.print_player_scores(rnd)
            term.wait_for_enter_key()
            term.clear()
            self.start_new_round()
            rnd += 1

if __name__ == '__main__':
    game = Game()
    term.clear()
    game.print_instructions()
    game.get_player_setup()
    while True:
        game.start()
        inp = None
        while inp not in ['s', 'c', 'q']:
            inp = input('(S)tart a new game, (C)ontinue a new game with previous players, or (Q)uit? ').strip().lower()
        if inp[0].lower() == 'q':
            break
        elif inp[0].lower() == 's':
            game.get_player_setup()
            game.start_new_round()
        elif inp[0].lower() == 'c':
            game.start_new_round()
